/**
 * 默认报告样式
 *
 * Created by snowingsea on 2021/01/19.
 */
const fs = require('fs-extra');
const path = require('path');
// eslint-disable-next-line import/no-extraneous-dependencies
const canvas = require('canvas');
const compressing = require('compressing');
const randomString = require('randomstring');
const eCharts = require('echarts');
const moment = require('moment');

const mongoose = require('mongoose');
const projectVersions = require('./../../../project-versions');
const defects = require('../../../defects');
const BaseReporter = require('./../base-report');
const utility = require('./../../../utility');
const gitClient = require('../../../git/git-client');
const files = require('../../../files');
const rulesModels = require('../../../rules');
const User = require('../../../mongo/user');
const Function = require('../../../mongo/function');
const SummaryExcel = require('./template/excel/summary');
const ProgressIndicator = require('../../../progress-indicator');

const levelKeys = {
  1: 'mandatory',
  2: 'required',
  3: 'advisory',
};
const levelKeyCn = {
  1: '强制',
  2: '要求/必要/强烈推荐',
  3: '建议/推荐',
};
// 导出报告的分类
const exportReportCategory = {
  file: 1, // 按文件分类
  ruleSet: 2, // 按规则集分类
};

class DefaultReporter extends BaseReporter {
  constructor(versionId, locale, version = null, project = null, creator = null, outputDir = null, format, filters = {},
    reportCategory = exportReportCategory.file, baseDir = null, reportRange = null) {
    super(versionId, locale, version, project, creator, outputDir, baseDir || __dirname, format);
    this.rulesMap = new Map();
    this.filters = filters;
    this.ableReportCategory = parseInt(reportCategory, 10) === exportReportCategory.ruleSet;
    this.reportRange = reportRange;
    this.noFiles = !!(reportRange && reportRange.indexOf(2) < 0);
  }

  /**
   * 报告生成
   */
  async generate(progressIndicator = new ProgressIndicator()) {
    const result = {
      cachePath: this.outputDir,
    };
    const [filesProgressIndicator, summaryProgressIndictor] = progressIndicator.split([0.9, 0.1]);
    const { versionPath } = await this.spliteByProjectVersion((versionDir) => fs.ensureDir(versionDir));
    if (this.format === 'xlsx') {
      await this.generateExcelSummary(filesProgressIndicator, path.join(versionPath, '软件静态分析违反编码规则表.xlsx'));
    } else {
      this.versionRuleSets = await defects.getDefectTree(this.versionId, this.filters);
      await this.handleFileRootPath();
      if (!this.reportRange || this.reportRange.indexOf(2) > 0) {
        await this.spliteByFile(null, versionPath, filesProgressIndicator);
      }
      if (!this.reportRange || this.reportRange.indexOf(1) > 0) {
        await this.generateSummary();
      }
    }
    const fileName = `${this.project.projectName}-${this.version.versionName}-${this.getReportName()}-${this.format}.zip`;
    result.reportPath = path.join(this.outputDir, fileName);
    const reportsDir = path.join(this.outputDir, this.project.projectName);
    await compressing.zip.compressDir(reportsDir, result.reportPath);
    summaryProgressIndictor.finish();
    return result;
  }

  async generateExcelSummary(filesProgressIndicator, outputPath) {
    const versionDefects = await defects.getDefects(this.versionId, this.filters);
    const filesRulesMap = new Map();
    const fileFuncsMap = new Map();
    (await Function.find({ versionId: this.versionId }, ['fileId', 'functionName', 'location', 'endLocation']).lean())
      .forEach((func) => {
        const fileId = func.fileId.toString();
        if (!fileFuncsMap.has(fileId)) {
          fileFuncsMap.set(`${func.fileId.toString()}`, {});
        }
        if (!func.endLocation) {
          return;
        }
        fileFuncsMap.get(fileId)[`${func.location.line}-${func.endLocation.line}`] = func.functionName;
      });
    const defectCount = versionDefects.length;
    const statusToChinese = {
      0: '待修改',
      1: '已修改',
      2: '忽略',
      3: '误报',
      4: '已说明',
    };
    await utility.arrayChunkOperation(versionDefects, 100, (defect) => {
      const fileId = defect.fileId.toString();
      const key = `${defect.defectId.toString()}`;
      if (!filesRulesMap.has(key)) {
        filesRulesMap.set(key, {
          fileName: defect.fileName,
          functionNames: [],
          lines: [],
          ruleName: defect.ruleName,
          ruleCode: `${defect.ruleSetName} ${defect.ruleCode}`,
          description: defect.description,
          status: statusToChinese[defect.status],
        });
      }
      const defectBeginLine = defect.locations.begin.line;
      const defectEndLine = defect.locations.end.line;
      const lineKey = defectBeginLine === defectEndLine ? defectBeginLine : `${defectBeginLine}-${defectEndLine}`;
      if (!filesRulesMap.get(key).lines.includes(lineKey)) {
        filesRulesMap.get(key).lines.push(lineKey);
      }
      if (!fileFuncsMap.has(fileId)) {
        return;
      }
      const funcs = fileFuncsMap.get(fileId);
      let funcLineKey;
      let currentLine;
      Object.keys(funcs).forEach((fkey) => {
        const [funcBeginLine, funcEndLine] = fkey.split('-');
        if (defectBeginLine > funcBeginLine || funcEndLine < defectEndLine) {
          return;
        }
        currentLine = currentLine ? Math.min(currentLine, funcBeginLine) : funcBeginLine;
        funcLineKey = fkey;
      });
      if (funcLineKey && !filesRulesMap.get(key).functionNames.includes(funcs[funcLineKey])) {
        filesRulesMap.get(key).functionNames.push(funcs[funcLineKey]);
      }
      filesProgressIndicator.advance(100 / defectCount);
    });
    const data = [];
    let index = 0;
    filesRulesMap.forEach((value,) => {
      index += 1;
      data.push({
        index,
        ...value,
        functionNames: value.functionNames.length === 0 ? 'Whole program' : value.functionNames.toString(),
        lines: value.lines.length === 0 ? '' : value.lines.toString(),
        userName: this.creator.username,
        // description: '',
      });
    });
    await (new SummaryExcel()).generate(data, outputPath);
  }

  /**
   * 按文件分割
   * @return {Promise}
   */
  async spliteByFile(callback = null, versionDir, progressIndicator) {
    const resFiles = await defects.getDefectFileByVersionId(this.versionId, this.filters);
    return this.handleFiles(resFiles, versionDir, callback, progressIndicator);
  }

  /**
   *按规则集构建新的规则集分类导出数据
   */
  async getVersionDefects(ruleProject = {}) {
    const filesDefects = await defects.getDefectByGroupFile(this.versionId, this.filters);
    const defectSourceCodeMap = new Map();
    const ruleDefectsMap = new Map();
    await utility.arrayChunkOperation(filesDefects, 10, (file) => this.generateDefectByFile(file, file.defects, defectSourceCodeMap, ruleDefectsMap));
    // 构建新的规则集分类输出结构数据
    const newRules = [];
    this.versionRuleSets.ruleSets.forEach(({ levels, ruleSetName }) => {
      if (!levels || levels.length === 0) {
        return;
      }
      levels.forEach(({ rules }) => {
        if (!rules || rules.length === 0) {
          return;
        }
        rules.forEach((rule) => {
          let newDefects = [];
          if (!ruleDefectsMap.has(rule.ruleId.toString())) {
            return;
          }
          ruleDefectsMap.get(rule.ruleId.toString()).forEach((defectId, index) => {
            const key = defectId.toString();
            if (defectSourceCodeMap.has(key)) {
              const defect = defectSourceCodeMap.get(key);
              newDefects.push({
                filePath: defect.filePath,
                locations: defect.locations,
                lines: defect.lines,
                index: index + 1,
                status: defect.status,
                description: defect.description,
              });
            }
          });
          newDefects = newDefects.sort((a, b) => {
            if (a.filePath > b.filePath) return 1;
            if (a.filePath < b.filePath) return -1;
            if (a.locations.begin.line !== b.locations.begin.line) {
              return a.locations.begin.line - b.locations.begin.line;
            }
            return a.locations.begin.column - b.locations.begin.column;
          });
          const newRule = {
            ruleSetName,
            ruleName: rule.name,
            ruleCode: rule.ruleCode,
            total: rule.total,
            defects: newDefects,
            levelName: rulesModels.ruleLevelEum[rule.originalLevel].name,
            level: rulesModels.ruleLevelEum[rule.originalLevel].mainLevel,
          };
          Object.keys(ruleProject).forEach((key) => {
            newRule[key] = rule[key];
          });
          newRules.push(newRule);
        });
      });
    });
    return newRules;
  }

  async handleFileRootPath() {
    const rootPath = gitClient.getLocalRepoPath({ _id: this.version.creatorId }, this.project);
    const user = await User.findOne({ _id: mongoose.Types.ObjectId(this.version.creatorId) });
    await gitClient.checkoutBranch(user, this.project, this.version, this.version.currentCommitId.toString());
    this.rootPath = rootPath;
  }

  /**
   * 处理单个文件，生成缺陷报告
   * @param {Object}} file 单个文件信息
   * @param {String} versionDir 版本目录
   * @returns {Promise}
   */
  async handleFile(file, versionDir) {
    const reportPath = `${path.join(versionDir, path.dirname(file.path), file.fileName)}`;
    await fs.ensureDir(path.dirname(reportPath));
    const allRulesBySet = await this.generateDefectByFile(file);
    const data = {
      fileName: file.fileName,
      allRulesBySet,
      reportName: file.fileName,
      ableReportCategory: this.ableReportCategory,
      headerRightContent: '静态分析报告',
    };
    const outputPath = await this.generateFileReport(data, reportPath);
    return { reportPath: outputPath };
  }

  /**
    * 过滤规则集数据
    * @param {Array[Object]} ruleSets 规则集
    * @param {Array[Object]} fileDefects 文件缺陷
    * @returns
    */
  handleDefectsByRuleSets(ruleSets, fileDefects) {
    return ruleSets
      // 只展示有缺陷的rule set
      .filter((set) => set.total)
      .map((set) => ({
        ...set,
        rules: set.levels.reduce((arr, lvl) => arr.concat(lvl.rules.map((r) => ({
          ...r,
          lvl: lvl.level,
          level: rulesModels.ruleLevelEum[r.originalLevel].name,
        }))), [])
          // 只展示有缺陷的rule
          .filter((r) => r.total)
          .map((v, index) => ({ ...v, index: index + 1 }))
          .reduce((ret, rule) => {
            let defects1;
            if (fileDefects) {
              defects1 = fileDefects.filter((d) => d.ruleId.equals(rule.ruleId))
                .map((defect, index) => ({
                  ...defect,
                  ruleSetName: set.ruleSetName,
                  index: index + 1,
                }));
              if (defects1.length > 0) {
                rule.total = defects1.length;
                ret[`level${rule.lvl}`].push({
                  ...rule,
                  defects: defects1,
                });
              }
            } else {
              ret[`level${rule.lvl}`].push({
                ...rule,
                defects: defects1,
              });
            }
            return ret;
          }, {
            level1: [], level2: [], level3: [],
          }),
      }));
  }

  /**
   * 处理单个文件的缺陷数据处理
   */
  async generateDefectByFile(file, fileAllDefects, defectSourceCodeMap, ruleDefectsMap) {
    const { ruleSets } = this.versionRuleSets;
    const { fileId } = file;
    if (!fileAllDefects) {
      fileAllDefects = await defects.getDefects(
        this.versionId,
        {
          fileId,
          ruleSets: this.filters.ruleSets,
          levels: this.filters.levels,
          status: this.filters.defectStatus,
        }
      );
    }
    const lines = await files.getLinesOfRemoteFileStream(fileId, {});
    const lastLine = lines.slice(-1);
    const statusToChinese = {
      0: '待修改',
      1: '已修改',
      2: '忽略',
      3: '误报',
      4: '已说明',
    };
    await utility.arrayChunkOperation(fileAllDefects, 1000, async (defect) => {
      defect.status = statusToChinese[defect.status];
      defect.description = defect.description ? defect.description : '';
      const { begin, end } = defect.locations;
      if (!begin || !end) {
        return;
      }
      if (!begin.line) {
        return;
      }
      const startLine = begin.line;
      const endLine = end.line ? end.line : startLine;
      // 处理可能不存在的行列信息
      begin.column = begin.column ? begin.column : '-';
      end.line = end.line ? end.line : '-';
      end.column = end.column ? end.column : '-';
      // 读取的行数超出文件内容行数的全部补空
      if (lastLine.length === 1 && endLine > lastLine[0].line) {
        const lineNumber = lastLine[0].line + 1;
        for (let i = lineNumber; i <= endLine; i += 1) {
          lines.push({
            line: lineNumber,
            content: '',
          });
        }
      }
      defect.lines = [];
      lines.forEach((l) => {
        // 过滤非法字符
        // eslint-disable-next-line no-control-regex
        l.content = l.content.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
        // 获取的代码行数,保留缺陷前三行
        let newLine;
        if ((startLine <= 3 && l.line <= 3) || (l.line >= startLine - 3 && l.line <= endLine)) {
          newLine = { ...l };
        }
        // 标记缺陷行数为红色
        if (newLine && newLine.line >= startLine && newLine.line <= endLine) {
          newLine.red = true;
        }
        if (newLine) {
          defect.lines.push(newLine);
        }
      });
      if (defectSourceCodeMap) {
        defectSourceCodeMap.set(defect.defectId.toString(), { ...defect, filePath: file.filePath });
      }
      if (ruleDefectsMap) {
        if (!ruleDefectsMap.has(defect.ruleId.toString())) {
          ruleDefectsMap.set(defect.ruleId.toString(), []);
        }
        ruleDefectsMap.get(defect.ruleId.toString()).push(defect.defectId.toString());
      }
    });
    if (this.ableReportCategory) {
      return Promise.resolve();
    }
    return this.handleDefectsByRuleSets(ruleSets, fileAllDefects);
  }

  /**
   * 缺陷总览
   * @returns
   */
  async generateSummary() {
    const summary = {
      reportTime: moment().format('YYYY/MM/DD HH:mm:ss'),
      jumpAble: this.ableReportCategory && this.format === 'html',
      noFiles: this.noFiles,
      headerRightContent: '静态分析报告',
    };
    const { ruleSets, total: totalDefectCount } = this.versionRuleSets;
    const [
      { sourceFileCount, headerFileCount, numberOfLines },
      overview,
      levelStatusCount,
    ] = await Promise.all([
      projectVersions.fileCountInfo(this.versionId, this.version),
      defects.getVersionOverview(this.versionId, this.filters),
      defects.countByLevelAndStatus(this.versionId, this.filters),
    ]);
    const levelCountMap = levelStatusCount.reduce((obj, { level, total }) => {
      obj[levelKeys[level]] += total;
      return obj;
    }, {
      mandatory: 0,
      required: 0,
      advisory: 0,
    });
    const levelStatusMap = levelStatusCount.reduce((arr, { level, status, total }) => {
      let lvlObj = arr.find((lvl) => lvl.lvl === level);
      if (!lvlObj) {
        lvlObj = {
          lvl: level,
          level: levelKeyCn[level],
          total,
          status_0: 0,
          status_1: 0,
          status_2: 0,
          status_3: 0,
          status_4: 0,
        };
        arr.push(lvlObj);
      }
      lvlObj[`status_${status}`] += total;
      return arr;
    }, []);
    const ruleSetMap = ruleSets.reduce((
      arr, { ruleSetName, total: defectCount, levels }
    ) => arr.concat({
      ruleSetName,
      defectCount,
      ruleCount: levels.reduce((cnt, lvl) => cnt + lvl.rules.length, 0),
    }), []);
    summary.fileCount = sourceFileCount + headerFileCount;
    summary.numberOfLines = numberOfLines;
    summary.projectName = overview.projectName;
    summary.versionName = overview.versionName;
    summary.defectsCount = totalDefectCount;
    summary.kloc = numberOfLines > 0 ? (
      Math.ceil((levelCountMap.mandatory / numberOfLines) * 1000)
      + Math.ceil((levelCountMap.required / numberOfLines) * 1000)
      + Math.ceil((levelCountMap.advisory / numberOfLines) * 1000)) : 0;
    summary.mandatory = levelCountMap.mandatory;
    summary.required = levelCountMap.required;
    summary.advisory = levelCountMap.advisory;
    summary.ruleSetMap = ruleSetMap.map((v, index) => ({ ...v, index: index + 1 }));
    summary.levelStatusMap = levelStatusMap.sort((l1, l2) => (l1.lvl > l2.lvl ? 1 : -1));
    // top10
    summary.rulesTop10 = overview.rules.map((v, index) => ({ ...v, index: index + 1 }));
    summary.filesTop10 = overview.files.map((v, index) => ({ ...v, index: index + 1 }));
    // 处理规则集
    summary.allRulesBySet = this.handleDefectsByRuleSets(ruleSets);
    const { output: pieImagePath } = await this.generateDefectLevelCountPieImage(
      levelCountMap,
      this.getImageSize().pieImage
    );
    summary.pieImage = pieImagePath;

    // eslint-disable-next-line max-len
    const { output: rulesBarChatPath } = await this.generateDistributionImage(summary.rulesTop10, this.getImageSize().rulesDistributionImage);
    summary.rulesDistributionImage = rulesBarChatPath;

    // eslint-disable-next-line max-len
    const { output: filesBarChatPath } = await this.generateDistributionImage(summary.filesTop10.map((item) => ({
      ...item,
      name: item.fileName,
    })), this.getImageSize().filesDistributionImage);
    summary.filesDistributionImage = filesBarChatPath;

    return this.generateSummaryReport(
      summary,
      path.join(
        this.outputDir,
        this.project.projectName,
        this.version.versionName,
        'summary'
      )
    );
  }

  generateSummaryReport(data, output) {
    if (this.format === 'xlsx') {
      return (new SummaryExcel()).generate(data, `${output}.xlsx`);
    }
    return this.generateReport('summary-template', data, output, { isSummary: true });
  }

  getImageSize() {
    return {
      rulesDistributionImage: { height: 160, width: 540 },
      filesDistributionImage: { height: 160, width: 540 },
      pieImage: { height: 250, width: 300 },
    };
  }

  generateDefectLevelCountPieImage(resultData, size) {
    const shouldGray = Object.values(resultData).every((v) => !v);
    return new Promise((resolve, reject) => {
      const styles = {
        mandatory: {
          name: '强制',
          color: shouldGray ? '#f2f2f2' : '#ca3e36',
        },
        required: {
          name: '要求/必要/强烈推荐',
          color: shouldGray ? '#f2f2f2' : '#fdbb36',
        },
        advisory: {
          name: '建议/推荐',
          color: shouldGray ? '#f2f2f2' : '#4dd5b3',
        },
      };
      const serie = {
        name: 'results',
        type: 'pie',
        data: [],
        // top: '10%',
        avoidLabelOverlap: true,
        // radius: '70%',
        radius: ['30%', '50%'],
        startAngle: 180,
        minShowLabelAngle: 2e-8,
        label: {
          normal: {
            textStyle: {
              fontSize: 30,
              color: '#515151',
            },
          },
        },
        labelLine: {
          normal: {
            length: 20,
            length2: 40,
          },
        },
        itemStyle: {
          normal: {
            color: (params) => styles[params.data.kindName].color,
          },
        },
      };
      Object.keys(resultData).forEach((kindName) => {
        if (!styles[kindName]) {
          return;
        }
        serie.data.push({
          value: resultData[kindName],
          name: `${styles[kindName].name}: ${resultData[kindName]}`,
          kindName,
        });
      });
      const options = {
        title: {
          show: false,
          text: '缺陷级别数量统计',
          x: 'center',
          top: 0,
          textStyle: {
            fontweight: 'bold',
            fontSize: 30,
          },
        },
        legend: {
          show: false,
          orient: 'vertical',
          x: 'left',
        },
        series: [serie],
      };
      const outputPath = this.getImagePath();
      utility.writeFile(DefaultReporter.getEChartsImage(options, size), outputPath)
        .then(() => {
          resolve({ output: outputPath });
        })
        .catch(reject);
    });
  }

  generateDistributionImage(resultData, size) {
    return new Promise((resolve, reject) => {
      const options = {
        grid: {
          top: '10%',
          bottom: '20%',
          left: '15%',
          right: '5%',
        },
        legend: {
          height: '10%',
          textStyle: {
            fontSize: 30,
          },
        },
        tooltip: {},
        xAxis: {
          type: 'category',
          data: resultData.map((item) => item.name),
          axisTick: { alignWithLabel: true },
          axisLabel: {
            interval: 0,
            rotate: 15,
            textStyle: {
              color: '#000',
              fontSize: 30,
            },
            formatter(value) {
              return (value.length > 12 ? (`${value.slice(0, 12)}...`) : value);
            },
          },
          axisLine: {
            show: true,
            interval: 0,
            lineStyle: {
              color: 'RGB(210,221,217)',
            },
          },
        },
        yAxis: {
          axisLabel: {
            fontSize: 30,
          },
        },
        series: [{
          type: 'bar',
          data: resultData.map((item) => item.total),
          barWidth: 60,
          barGap: 0.5,
          showBackground: true,
          backgroundStyle: {
            opacity: 0.35,
            color: '#E7F0FF',
            borderRadius: [10, 10, 0, 0],
          },
          itemStyle: {
            // 图形的形状
            color: '#5db8ff',
            barBorderRadius: [20, 20, 0, 0],
          },
        }],
      };
      const outputPath = this.getImagePath();
      utility.writeFile(DefaultReporter.getEChartsImage(options, size), outputPath)
        .then(() => {
          resolve({ output: outputPath });
        })
        .catch(reject);
    });
  }

  /**
   * 获取图片路径
   * @return {string}
   */
  getImagePath() {
    return path.join(this.outputDir, `${randomString.generate(16)}.png`);
  }

  /**
   * 获取EChart图表数据
   * @param {Object} options 配置信息
   * @param {Object} canvasSize 画布大小
   * @return {Buffer}
   */
  static getEChartsImage(options, canvasSize) {
    const ratio = 4;
    canvas.registerFont(path
      .join(__dirname, '..', 'fonts', 'SourceHanSansCN-Bold.otf'), { family: 'Arial' });
    const imageCanvas = canvas.createCanvas(ratio * canvasSize.width, ratio * canvasSize.height);
    eCharts.setCanvasCreator(() => imageCanvas);
    const chart = eCharts.init(imageCanvas);
    chart.setOption(options);
    // return imageCanvas.toBuffer();
    const buf = imageCanvas.toBuffer();
    chart.dispose();
    return buf;
  }
}

/**
 * Export model definition object.
 */

module.exports = DefaultReporter;
